package game.square;

import game.dungeon.position.Direction;
import game.square.temperature.Temperature;
import game.square.temperature.TemperatureScale;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A class of squares, who form the smallest unit of location in the game,
 * storing its temperature, humidity, slipperiness, inhabitability and
 * borders.
 * 
 * @invar	The borders of this square are proper borders.
 * 		  | hasProperBorders()
 * 
 * @version 1.0
 * @author Chris Desomer, Geert Heyman
 */
 
public abstract class SquareImpl implements Square{

	/**
	  * Initialize this new square implementation
	 *
	 * @param	slipperyMaterial
	 * 			Indicates if this new square is made of a slippery material.
	 * @param	possibleTeleportationDestination
	 * 			Indicates if this square is a possible destination for teleportation
	 * @post	The slippery material property of this square is set to
	 * 			the given slippery material boolean.
	 * 		  | this.isSlipperyMaterial() == slipperyMaterial
	 * @post	The indication of a possible teleportation destination is set to
	 * 			the given possible teleportation destination boolean.
	 * 		  | this.isPossibleTeleportationDestination = possibleTeleportationDestination
	 */
	@Raw
	public SquareImpl(boolean slipperyMaterial, boolean possibleTeleportationDestination) {
		this.slipperyMaterial = slipperyMaterial;
		this.POSSIBLE_TELEPORTATION_DESTINATION= possibleTeleportationDestination;
	}
	
	/**
	 * Return the temperature of this square
	 */
	 @Basic
	public abstract Temperature getTemperature();

	/**
	 * Return the minimum temperature at which heat damage occurs.
	 */
	@Basic
	@Immutable
	public static int getMinTempWithHeatDamage() {
		return MIN_TEMP_WITH_HEAT_DAMAGE;
	}

	/**
	 * Return the cold damage of this square.
	 *  
	 * @return 	The cold damage of this square: one point for each 10 degrees 
	 * 			Celcius below -5 degrees Celcius. Rounded to the closest integer
	 * 			to zero.
	 * 		  | if (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) <= -15)
	 * 		  |		then result == (-15 - getTemperature().getTemperatureValue(TemperatureScale.CELCIUS)
	 * 		  |						)/10 + 1
	 *@result	Otherwise the result is 0
	 *		  |  	else result == 0 		
	 */
	public int getColdDamage() {
	int tempInCelcius = getTemperature().getTemperatureValue(TemperatureScale.CELCIUS);
	if (tempInCelcius <= -15)
		return (-15 - tempInCelcius)/10 + 1;	
	return 0;
	}

	
	/**
	 * Return the heat damage of this square.
	 * 
	 * @return 	The heat damage of this square: one point for each time the heat
	 *         	damage scale fits into the current temperature in Celcius.
	 * 		  | if (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS)) 
	 * 		  |  		>= getMinTempWithHeatDamge())
	 *		  |		then result == (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS)
	 *		  |						 - getMinTempWithHeatDamage())/getScaleHeatDamage() + 1;	
	 *		  |	else result == 0;
	 */
	public int getHeatDamage() {
		int tempInCelcius = getTemperature().getTemperatureValue(TemperatureScale.CELCIUS);
		if (tempInCelcius >= getMinTempWithHeatDamage())
			return (tempInCelcius - getMinTempWithHeatDamage())/getHeatDamageUnitTemperature() + 1;	
		return 0;
	}

	/**
	 * Class variable that stores the value of the minimum temperature at which heat damage occurs
	 */
	private static final int MIN_TEMP_WITH_HEAT_DAMAGE = 35 + getHeatDamageUnitTemperature();

	/**
	 * Return the unit temperature that is used to calculate the heat damage,
	 * adding this value to a certain temperature above the minimum
	 * temperature with damage, makes the corresponding heat damage
	 * increase with one.
	 */
	@Basic
	@Immutable
	public static int getHeatDamageUnitTemperature() {
		return HEAT_DAMAGE_UNIT_TEMPERATURE;
	}

	/**
	 * Class variable that stores the value of the unit temperature that 
	 * is used to calculate the heat damage.
	 */
	private static final int HEAT_DAMAGE_UNIT_TEMPERATURE = 15;

	/**
	 * Check if the given value is a valid humidity.
	 * 
	 * @param 	humidity
	 * 			The humidity to check
	 */
	@Raw
	public abstract boolean canHaveAsHumidity(BigDecimal humidity);

	/**
	 * Return the humidity of this square.
	 */
	@Basic
	public abstract BigDecimal getHumidity();

	
	/**
	 * Return the unit humidity value used to calculate the rust
	 * damage of this square.
	 */
	@Basic
	@Immutable
	public static int getRustDamageUnitHumidity() {
		return RUST_DAMAGE_UNIT_HUMIDITY;
	}

	/**
	 * A class variable that stores the unit humidity value :
	 * adding this value to a certain humidity above the minimum
	 * humidity with damage, makes the corresponding rust damage
	 * increase with one.
	 */
	private static final int RUST_DAMAGE_UNIT_HUMIDITY = 7;
	/**
	 * Class variable that stores the value of the minimum humidity
	 * that results in rust damage.
	 */
	private static final int MIN_HUMIDITY_WITH_RUST_DAMAGE = 30 + getRustDamageUnitHumidity();

	/**
	 * Return the minimum humidity that results in rust damage.
	 */
	@Basic
	@Immutable
	public int getMinHumidityWithRustDamage() {
		return MIN_HUMIDITY_WITH_RUST_DAMAGE;
	}

	/**
	 * a variable that indicates if this square is made of a slippery material or not
	 */
	private final boolean slipperyMaterial;

	/**
	 * Calculate the rust damage of this square.
	 * 
	 * @return 	The rust damage of this square: one point for each time the rust
	 *         	damage scale fits into the current humidity of this square.
	 * 		  | if ( getHumidity().compareTo(getMinHumidityWithRustDamage()) == -1)
	 *		  |		then result == 0	
	 *		  |	else result == getHumidity().subtract(getMinHumidityWithRustDamage()).
	 *								divide(getScaleRustDamage()).intValue() + 1	
	 */	 
	public int getRustDamage() {
		if (getHumidity().intValue() >= getMinHumidityWithRustDamage())
			return  Math.round((getHumidity().floatValue() - getMinHumidityWithRustDamage()))
						/(getRustDamageUnitHumidity()) + 1;
		return 0;
	}
	
	/**
	 * Return the slipperiness of this square.
	 * 
	 * @return  True if the floor of a square is made of a slippery material
	 * 		  | if(isSlipperyMaterial())
	 * 		  |		then result == true
	 * @result  False if the humidity of this square is lower or equal to 10
	 * 		  | if(getHumidity().floatValue() <= 10)
	 * 		  | 	then result == false
	 * @result  True if the temperature in Celcius of this square is negative
	 * 		  | if (getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) < 0)
	 * 		  |		then result == true
	 * @result	Otherwise true if the humidity is 100
	 * 		  |	getHumidity().compareTo((BigDecimal.valueOf(100))) == 0
	 */
	public boolean isSlippery() {
		if (isSlipperyMaterial())
			return true;
		if (getHumidity().floatValue() <= 10)
			return false;
		int tempInCelcius = getTemperature().getTemperatureValue(TemperatureScale.CELCIUS);
		if (tempInCelcius < 0)
			return true;
		return getHumidity().compareTo((BigDecimal.valueOf(100))) == 0;
	}
	
	/**
	 * Return if the floor of a square is made of a slippery material or not.
	 */
	@Basic
	@Immutable
	public boolean isSlipperyMaterial() {
		return slipperyMaterial;
	}

	/**
	 * Variable that stores whether  this square is terminated or not.
	 */
	private boolean terminated;

	/**
	 * Return a measure for the inhabitability of this square.
	 * 
	 * @return  The measure of inhabitability is calculated thus: - sqrt(heat
	 *          damage^3) / sqrt( 101 - humidity) - sqrt(cold damage) 
	 *        | result == -(Math.pow(getHeatDamage(), 1.5)/ 
	 *        | 	Math.pow(101-getHumidity().doubleValue(),0.5)) 
	 *        | 	- Math.pow(getColdDamage(),0.5)
	 */
	public double getInhabitability() {
		double heatDamageFactor = Math.pow(getHeatDamage(), 1.5);
		double coldDamageTerm = Math.pow(getColdDamage(),0.5);
		double humidityFactor = Math.pow(101-getHumidity().doubleValue(),0.5);
		return -(heatDamageFactor / humidityFactor) - coldDamageTerm;
	}
	
	/**
	 * Check if this square is terminated.
	 */
	@Basic
	public boolean isTerminated() {
		return terminated;
	}

	/**
     * Terminate this square.
     *
     * @post    This square is terminated.
     *        | new.isTerminated()
     * @effect	All borders belonging to this square are terminated if
     * 			they don't share the border with an effective square.
     * 		  | 	For each direction in Direction.values()
     * 		  | 		if (hasAsBorderInDirection(direction))
     * 		  |				removeBorderInDirection(border)
     */
	public void terminate() {
		for (Direction direction : Direction.values()) {
			if (hasBorderInDirection(direction))
				removeBorderInDirection(direction);
		}
		terminated = true;
		
	}

	/**
	 * Check if the given number of borders is valid for squares.
	 * @param 	nbBorders
	 * 			The number of borders to check.
	 * @return	If the given number of borders is greater than 6
	 * 			result is false.
	 * 		  | result == (nbBorders <= 6)
	 */
	public static boolean isValidNbBorders(int nbBorders) {
		return nbBorders <= 6;
	}

	/**
	 * Return a copy of the borders of this square.
	 */
	@Basic
	public Map<Direction, Border> getBorders() {
		return new HashMap<Direction, Border>(borders);
	}
	
	/**
	 * Get the direction of the given border in regard to this square.
	 * 
	 * @param	border
	 * 			The border to get the direction from.
	 * @return	The direction of the given border, if the given border 
	 * 			doesn't border this square than the result is null.
	 * 		  | result ==
	 *        |   {direction in getBorders().values() : getBorders().get(direction) == border}
	 */
	public Direction getDirectionOf(Border border) {
		for (Direction direction : borders.keySet()) {
			if (borders.get(direction) == border)
				return direction;
		}
		return null;
	}
	
		
	/**
	 * Check whether this square has the given border as one
	 * of its borders.
	 * 
	 * @param	border
	 * 			The border to check.
	 * @return	True if and only if the given border has an effective
	 * 			direction
	 * 		  | result == (getDirectionOf(border) != null)
	 */
	@Raw
	public boolean hasAsBorder(@Raw Border border) {
		return getDirectionOf(border) != null;
	}
	
	/**
	 * Check if this square has an effective border in the given direction.
	 * 
	 * @param 	direction
	 * 			The direction to check for a border.
	 * @result	True if and only if this square has an effective border in
	 * 			the given direction.
	 * 		  | result == (getBorders.get(direction) != null) 
	 */
	public boolean hasBorderInDirection(Direction direction) {
		return (borders.get(direction) != null);
	}
	
	/**
	 * Check whether this square can have the given border
	 * as one of its borders in the given direction.
	 * 
	 * @param	border
	 * 			The border to check.
	 * @param	direction
	 * 			The direction of the given border.
	 * @return	False if the given direction is not effective
	 * 		  | if (direction == null)	
	 * @result	False if this square can't have the border in any direction.
	 * 		  | if (!canHaveAsBorder(border))
	 * 		  |		then result == false
	 * @result	Otherwise, false if the direction is floor and the given border
	 * 			is a door instance
	 * 		  |	else if (direction == Direction.FLOOR && (border instanceof Door))
	 * 		  |		then result == false
	 * @result	Otherwise the result is true.
	 * 		  | else result == true
	 */
	@Raw
	public boolean canHaveAsBorderInDirection(@Raw Border border, Direction direction) {
		if (direction == null)
			return false;
		if (!canHaveAsBorder(border))
			return false;
		if (direction == Direction.FLOOR
				&& (border instanceof Door))
			return false;
		return true;
	}
	
	/**
	 * Check whether this square can have the given border
	 * as one of its borders.
	 * 
	 * @param   border
	 *          The border to check.
	 * @return  False if the given border is not effective.
	 * 		  |	if (border == null)
	 * 		  |		then result == false
	 * @result	Otherwise, false if the given border or this square are terminated.
	 * 		  | else if (!border.isTerminated() || isTerminated())
	 * 		  |		then result == false
	 * @result	Otherwise, false if the given border can't have this
	 * 			square is one of the squares it borders.
	 * 		  |	else if (!border.canHaveAsSquare(this))
	 * 		  |		then result == false
	 * @result	Otherwise, false if the new number of borders is
	 * 			greater than six.
	 * 		  | else if(getNbBorders() + 1 > 6
	 * 		  |		then result == false
	 * @result	Otherwise, the result is true
	 * 		  | else result == true
	 */
	@Raw
	public boolean canHaveAsBorder(@Raw Border border) {
		// the concept of no fysical border, is still an object of 
		// the class border, so a border must be effective.
		if (border == null)
			return false;
		if (border.isTerminated() || isTerminated())
			return false;
		if (!border.canHaveAsSquare(this))
			return false;
		// the number of directions will stay the same.
		if (getNbBorders()+1 > 6)
			return false;
		return true;
	}

	/**
	 * Check whether this square has proper borders.
	 * 
	 * @return	False, if this square has no borders and is not terminated
	 * 			(the concept of	'no physical border' counts as a border).
	 * 		  |	if (getBorders().isEmpty() && !isTerminated())
	 * 		  |		then result == false
	 * @result	False if this square has more than three doors.
	 * 		  | if (getNbOfDoors() > 3)
	 * 		  |		then result == false
	 * @result	Otherwise false if at least one of the borders of this square
	 * 			is not effective or terminated.
	 * 		  | For each border in Border
	 * 		  |		if (hasAsBorder(border) && (border == null || border.isTerminated()))
	 * 		  |			then result == false
	 * @result	Otherwise false if at least one of the borders of this square doesn't reference
	 * 			this square.
	 * 		  | For each border in Border
	 * 		  |		if (hasAsBorder(border) && (!border.isBorderOf(this))
	 *		  |			then  result == false
	 * @result	Otherwise, false if at least one of the borders of this square can't have that
	 * 			border in the given direction.
	 * 		  |	For each border in Border
	 * 		  |		if (hasAsBorder(border) && !canHaveAsBorderInDirection(border,getDirectionOf(border))
	 * 		  |			then result == false
	 * @result	Otherwise, false if at least one of the borders of this square with an effective neighbor 
	 * 			doesn't has the same border in opposite directions.
	 * 		  |	For each border in Border
	 * 		  |		if (hasAsBorder(border) && border.getNeighborSquareFrom(this) != null &&
	 * 		  |				border.getNeighborSquareFrom(this).getDirectionOf(border) != 
	 * 		  |					getDirectionOf(border).getOppositeDirection())
	 * 		  |			then result == false
	 * @result	Otherwise the result is true.
	 * 		  | else result == true
	 */
	@Raw
	public boolean hasProperBorders() {
		if (borders.isEmpty() && !isTerminated())
			return false;
		if (getNbOfDoors() > 3 )
			return false;
		for (Border border : borders.values()) {
			if (border == null)
				return false;
			if (border.isTerminated())
				return false;
			if (!border.isBorderOf(this))
				return false;
			if (!canHaveAsBorderInDirection(border,getDirectionOf(border)))
				return false;
			Square neighborSquare = border.getNeighborSquareFrom(this);
			if (neighborSquare != null && neighborSquare.getDirectionOf(border) != getDirectionOf(border).getOppositeDirection())
				return false;
		}
		return true;
	}

	/**
	 * Return the number of borders in this square.
	 * 
	 * @return  The total number of borders collected in this square.
	 *        | result ==
	 *        |   card({borders:Border | hasAsBorder(border)})
	 */
	public int getNbBorders() {
		return borders.size();
	}
	
	/**
	 * Calculate the number of doors this square has.
	 * 
	 * @return	The number of borders of this square of type Door
	 * 		  | result == card({doors:Door | hasAsBorder(door)})
	 * 
	 */
	public int getNbOfDoors() {
		int nbOfDoors = 0;
		for (Border border : borders.values()){
			if (border instanceof Door)
				nbOfDoors++;
		}
		return nbOfDoors;
	}

	/**
	 * Variable referencing a map collecting all the borders
	 * registered in this square.
	 * 
	 * @invar   The referenced map is effective.
	 *        | borders != null
	 * @invar   Each key registered in the map is an effective
	 *          Direction.
	 *        | for each key in borders.keySet():
	 *        |   (key != null)
	 * @invar   Each value associated with a key in the map is
	 *          an effective, non-terminated border involving this
	 *          square and involving a square whose code is identical
	 *          to the associating key.
	 *        | for each key in borders.keySet():
	 *        |   (borders.get(key) != null) &&
	 *        |   (! borders.get(key).isTerminated()) &&
	 *        |   (borders.get(key).getSquare() == this) &&
	 *        |   (borders.get(key).getSquare().getCode().equals(key))
	 */
	private final Map<Direction, Border> borders = new HashMap<Direction, Border>();

	/**
	 *  Return the border of this square in the given direction
	 */
	@Basic
	public Border getBorderInDirection(Direction direction) {
		return borders.get(direction);
	}

	/**
	 * Return the neighbor of this square in the given direction.
	 * @param 	direction
	 * 			The direction from where you want the neighbor.
	 * @return	If this square has a border in the given direction,
	 * 			the result is the square who shares the same border in 
	 * 			the given direction.
	 * 		  |	if (getBorders().get(direction) != null)
	 * 		  |		then result == getBorders().get(direction).getNeighborSquareFrom(this)
	 * @result	Otherwise, the result is the null reference.
	 * 		  | else result == null
	 */
	public Square getNeighborInDirection(Direction direction) {
		 if (getBorders().get(direction) == null)
			 return null;
		 return borders.get(direction).getNeighborSquareFrom(this);
	}

	/**
	 * Add the given border in the given direction to the set of borders registered in
	 * this square.
	 * 
	 * @param   border
	 *          The border to be added.
	 * @param	direction
	 * 			The direction in which the given border needs to be added.
	 * @post    The given border is registered as the border of this square in the
	 * 			given direction.
	 *        | new.getBorderInDirection(direction) == border.
	 * @effect	The given border has this square as one of the squares it borders.
	 * 		  | border.addBorderedSquare(this)
	 * @throws	IllegalStateException
	 * 			This square can't place the given border in the given direction
	 * 		  | !isPlaceableBorderInDirection(border,direction)
	 */
	public void addBorderInDirection(Border border, Direction direction)
			throws IllegalStateException {
				if (!isPlaceableBorderInDirection(border, direction))
					throw new IllegalStateException("The given border can't be placed on this square");
				borders.put(direction,border);
				border.addBorderedSquare(this);
			}

	/**
	 * Add the given border in the given direction to the set of borders registered in
	 * this square and to the set of borders in the given square (in the opposite direction
	 * of the given direction).
	 * 
	 * @param   border
	 *          The border to be added.
	 * @param	direction
	 * 			The direction in which the given border needs to be added.
	 * @param	neighbor
	 * 			The neighbor square to add the given border, in the opposite
	 * 			of the given direction, to.
	 * @post    The given border is registered as the border of this square in the
	 * 			given direction.
	 *        | new.getBorderInDirection(direction) == border.
	 * @post	The given border is registered as the border of the given neighbor
	 * 			in the opposite direction of the given direction.
	 * 		  | (new neighbor).getBorderInDirection(direction) == border
	 * @effect	The given border has this square as one of the squares it borders.
	 * 		  | border.addBorderedSquare(this)
	 * @effect	The given border has the given neighbor as one of the squares it borders.
	 * 		  | border.addBorderedSquare(neighbor)
	 * @throws	IllegalStateException
	 * 			This square can't place the given border in the given direction
	 * 		  | !isPlaceableBorderInDirection(border,direction)
	 * @throws	IllegalStateException
	 * 			The given neighbor can't place the given border in the opposite
	 * 			direction of the given direction.
	 * 		  | !neighbor.isPlaceableBorderInDirection(border, direction.getOppositeDirection())
	 */
	public void addBorderInDirection(Border border, Direction direction, SquareImpl neighbor)
			throws IllegalStateException {
				if (!isPlaceableBorderInDirection(border, direction)
						|| !neighbor.isPlaceableBorderInDirection(border, direction.getOppositeDirection()))
					throw new IllegalStateException(
							"The given border can't be placed on this square, or on the given square");
				try {
					setEquilibriumOfRoomNeighbors(border, direction, neighbor);
				} catch (ClassCastException e) {
					setBindings(border, direction, neighbor);
				}		
			}
	
	/*
	 * An auxiliary method to set an equilibrium between neighbors.
	 */
	private void setEquilibriumOfRoomNeighbors(Border border,
			Direction direction, SquareImpl neighbor) {
		Set<RoomSquare> thisPartOfRoom;
		Set<RoomSquare> neighborPartOfRoom;
		thisPartOfRoom = ((RoomSquare)this).getRoom();
		neighborPartOfRoom = ((RoomSquare)neighbor).getRoom();
		setBindings(border, direction, neighbor);
		if (border.isOpenBorder())
			((RoomSquare)this).setRoomEquilibrium(thisPartOfRoom, neighborPartOfRoom);
	}
	
	/*
	 * An auxiliary method to set the bindings between squares.
	 */
	private void setBindings(Border border, Direction direction, SquareImpl neighbor) {
		this.borders.put(direction,border);
		neighbor.borders.put(direction.getOppositeDirection(),border);
		border.addBorderedSquare(this);
		border.addBorderedSquare(neighbor);
	}

	/**
	 * Check if the given border is placeable at this square in the given 
	 * direction (at this moment).
	 * 
	 * @param   border
	 *          The border to check.
	 * @param	direction
	 * 			The direction in which the given border needs to be added.
	 * @return	False, if this square can't have the given border in the given direction
	 * 		  | if (!canHaveAsBorderInDirection(border,direction))
	 * 		  |		then result == false
	 * @result	False, if the given border is already placed.
	 * 		  |	if (border.isPlaced())
	 * 		  |		then result == false
	 * @result	False, if this square already has a border in the given direction.
	 * 		  |	if (hasBorderInDirection(direction))
	 * 		  | 	then result == false
	 * @result 	False, if this square already has the given border as one of its borders.
	 * 		  | if (hasAsBorder(border))
	 * 		  |		then result == false
	 * @result	False, if the given border is a door and this square already has two doors.
	 * 		  | if (getNbOfDoors() > 2 && border instance of Door)
	 * 		  |		then result == false
	 * @result	Otherwise the result is true
	 * 		  |	else result == true
	 */
	public boolean isPlaceableBorderInDirection(Border border, Direction direction) {
		if (!canHaveAsBorderInDirection(border,direction))
			return false;
		if (border.isPlaced())
			return false;
		if (hasAsBorder(border))
			return false;
		if (hasBorderInDirection(direction))
			return false;
		if (getNbOfDoors() > 2 && border instanceof Door)
			return false;
		return true;
	}

	/**
	 * Add the given square as a neighbor of this square in the given direction
	 * by setting the same border for both squares (for this square in the given
	 * direction, for the given square in the opposite direction of the given
	 * direction) : the strongest border is picked (e.g. a door is stronger than
	 * no border, but weaker than a wall).
	 * 
	 * @param 	square
	 * 			The square to add
	 * @param 	direction
	 * 			The direction (from this square) of the border the given 
	 * 			square should be added to.
	 * @post	This square has the same border in the given direction as
	 * 			the given square in the opposite of the given direction.
	 * 		  |	new.getBorderInDirection(direction) == (new square).getBorderInDirection(
	 * 		  |			direction.getOppositeDirection())
	 * @post	The new border for both squares is stronger than, or equally
	 * 			strong as their old borders.
	 * 		  |	new.getBorderInDirection(direction).isStrongerThan(getBorderInDirection(direction) &&
	 * 		  | (new square).getBorderInDirection(direction).isStrongerThan(getBorderInDirection(direction))
	 * @throws	IllegalArgumentException
	 * 			The given direction is not effective
	 * 		  |	direction == null
	 * @throws	IllegalArgumentException
	 * 			Then given square is not effective
	 * 		  |	square == null
	 * @throws	IllegalArgumentException
	 * 			This square already has a neighbor in the given direction 
	 * 			or the given square already has a neighbor in the opposite
	 * 			direction of the given direction.
	 * 		  | getNeighborInDirection(direction) != null ||
	 *		  |		square.getNeighborInDirection(direction
	 * 		  |			.getOppositeDirection()) != null
	 * 			
	 */
	public void addNeighborInDirection(SquareImpl square, Direction direction)
			throws IllegalArgumentException {
				if (direction == null)
					throw new IllegalArgumentException("The direction must be effective");
				if (square == null)
					throw new IllegalArgumentException("The given square must be effective");
				if (getNeighborInDirection(direction) != null
						|| square.getNeighborInDirection(direction
								.getOppositeDirection()) != null)
					throw new IllegalArgumentException(
							"The square already has a neighbor in the given direction");
				Border border = getStrongestBorderInDirection(square,direction).getNewEqualBorder();
				replaceBordersInDirectionWithBorder(direction,border,square);
				
			}

	/*
	 * Auxiliary method to replace a border in the given direction
	 * with the given border. If there is no border in the given direction
	 * the given border is just added in the given direction.
	 * 
	 * @Note: Here it is allowed for the given border to be placed already.
	 */
	private void replaceBordersInDirectionWithBorder(Direction direction,
			Border border, SquareImpl neighbor) {
				if (hasBorderInDirection(direction))
					removeBorderInDirection(direction);
				if (neighbor.hasBorderInDirection(direction.getOppositeDirection()))
					neighbor.removeBorderInDirection(direction.getOppositeDirection());
				this.addBorderInDirection(border, direction, neighbor);
			}

	/**
	 * Remove the border in the given direction from the borders registered in
	 * this square.
	 * 
	 * @param   direction
	 *          The direction of the border to remove.
	 * @pre     This square has a border in the given direction.
	 * 		  |	hasBorderInDirection(direction)
	 * @post    This square no longer has the given border as one
	 *          of its borders.
	 *        | ! new.hasAsBorder(this)
	 * @post	If this square has a neighbor in the given direction,
	 * 			the neighbor no longer has the shared border as one of
	 * 			its borders, but has a new border from the same class
	 * 			instead.
	 * 		  | if (getNeighborInDirection(direction) != null)
	 * 		  |		then new.getNeighborInDirection(direction).getBorderInDirection(direction.
	 * 		  |				getOppositeDirection()) != getNeighborInDirection(direction) &&
	 *		  |			 new.getNeighborInDirection(direction).getBorderInDirection(direction.
	 * 		  |				getOppositeDirection()).getClass() == getNeighborInDirection(direction).getClass()	 
	 */
	public void removeBorderInDirection(Direction direction) {
		assert hasBorderInDirection(direction);
		Border borderToRemove = borders.get(direction);
		SquareImpl neighbor = (SquareImpl)getNeighborInDirection(direction);
		Border equalBorderForNeighbor = borderToRemove.getNewEqualBorder();
		borders.remove(direction);
		if (neighbor != null){
			neighbor.borders.remove(direction.getOppositeDirection());
			neighbor.addBorderInDirection(equalBorderForNeighbor,
				direction.getOppositeDirection());
		}
		borderToRemove.terminate();
		
	}

	
	/*
	 * An auxiliary method that returns the strongest border in the given direction 
	 * (for this square and the given square).
	 */
	private Border getStrongestBorderInDirection(Square square, Direction direction) {
		Border borderThisSquare = borders.get(direction); 
		Border borderGivenSquare = square.getBorders().get(direction.getOppositeDirection()); 
		if (borderThisSquare == null && borderGivenSquare == null)
			return new NoPhysicalBorder();
		if (borderThisSquare == null)
			return borderGivenSquare;
		if (borderGivenSquare == null)
			return borderThisSquare;
		// both borders are effective.
		if (borderThisSquare.isStrongerThan(borderGivenSquare))
			return borderThisSquare;
		return borderGivenSquare;
	}

	/**
	 * Variable that stores the number of dungeons this square belongs to.
	 */
	private int nbOfDungeons;

	/**
	 * Get the number of dungeons this square belongs to.
	 */
	@Basic
	public int getNbOfDungeons() {
		return nbOfDungeons;
	}
	
	/**
	 * Checks if the given square is in the same room as this square. The
	 * complexity is linear with the amount of squares in the room of this
	 * square.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @return	Return true if the given square borders this square and 
	 * 			the border between them is an open border
	 *		  | if (square.isDirectRoomNeighborFrom(this))
	 *		  |		result == true
	 * @result	Else return true if a the square this square borders 
	 * 			with an openable border, is in the same room as the given square.
	 * 		  | For someSquare in Square 
	 * 		  |		if (someSquare.isDirectRoomNeighborFrom(this))
	 * 		  |			if (someSquare.isInSameRoomAs(square))
	 * 		  |				result == true
	 * @result	Else, the result is false.
	 * 		  |	else result == false
	 */
	public boolean isInSameRoomAs(Square square) {
		return getRoom().contains(square);
	}
	
	/**
	 * Check if the given square is a direct room neighbor from this square.
	 * 
	 * @param	square
	 * 			The square to check with.
	 * @return	True if  the given square and this square share
	 * 			the same openable border.
	 * 		  | For each border in getBorders().values()
	 * 		  |		if (border.getNeighborSquareFrom(this) == square &&
	 * 		  |		  border.isOpenBorder())
	 * 		  |			result == true
	 * @result	Otherwise, the result is false.
	 * 		  |	else result == false
	 */
	public boolean isDirectRoomNeighborFrom(Square square){
		for (Border border : getBorders().values()) {
			SquareImpl neighbor = border.getNeighborSquareFrom(this);
			if (neighbor == square && border.isOpenBorder())
				return true;
		}
		return false;
	}
	

	/**
	 * Increment the number of dungeons this square belongs to with one.
	 * 
	 * @post	The number of dungeons of this square is incremented with one.
	 * 		  | new.getNbOfDungeons() == getNbOfDungeons() + 1
	 */
	public void incrementNbOfDungeons() {
		nbOfDungeons++;
	}

	/**
	 *  Return a string representation of this square
	 *  
	 *  @return	the string representation of this square
	 *  	  |	"Square [temperature in Celcius=" + getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) 
	 *  	  |	+ ", humidity=" + getHumidity()	 + ", room size = "+ getRoom().size()+ "]"
	 */
	@Override
	public String toString() {
		return "Square [temperature in Celcius=" + getTemperature().getTemperatureValue(TemperatureScale.CELCIUS) + ", humidity=" + getHumidity()
				 + ", room size = "+ getRoom().size()+ "]";
	}
	
	/**
	 * Indicate if this square is a possible destination for teleportation
	 */
	@Basic
	@Immutable
	public boolean isPossibleTeleportationDestination(){
		return POSSIBLE_TELEPORTATION_DESTINATION;
	}
	
	/**
	 * Variable that stores if this square is a possible teleportation destination.
	 */
	private final boolean POSSIBLE_TELEPORTATION_DESTINATION;
	
	/**
	 * Return the squares that belong to the same room as this square.
	 * 
	 * @return {squares in Square | square.isInSameRoomAs(this)}
	 */
	public abstract Set<? extends Square> getRoom();

	/**
	 * Checks if the given square is reachable from this square. 
	 * 
	 * @param 	square
	 * 			The square to check reachability from.
	 */
	public abstract boolean canReachSquare (Square square);
}